在 Vue 3 中,Pinia 作為狀態管理庫,提供了靈活而強大的工具來管理應用的狀態。為了進一步提高 Pinia 的使用體驗,了解其底層機制和相關概念非常重要。本文將深入探討 Pinia 中的 effectScope
、Map
、WeakMap
、Set
、WeakSet
以及 private store
和 readonly store
的運作方式,並提供對應的實作範例。
effectScope
是 Vue 3 提供的一個響應式作用域控制機制,它允許我們在一個作用域中集中管理響應式狀態、計算屬性和監聽器,並能夠在需要時一鍵清除。Pinia 使用 effectScope
來管理 store 的響應式狀態,確保每個 store 的狀態在作用域被清理時自動停止追蹤。
import { shallowRef, effectScope } from 'vue';
// 創建一個新的 effectScope
const scope = effectScope();
scope.run(() => {
const count = shallowRef<number>(0);
// 使用 effectScope 管理的狀態
const increment = () => {
count.value++;
};
console.log('當前計數:', count.value);
increment();
console.log('增加後的計數:', count.value);
});
// 清理 scope 中的所有狀態
scope.stop();
在這個例子中,effectScope
管理了 count
和 increment
函數,當 scope.stop()
被調用時,所有在 scope 中的反應性依賴都會被清理。
在 Pinia 中,每個 store 都是由 effectScope
管理的,這意味著當我們銷毀 store 或不再使用時,可以通過停止該作用域來清理所有反應性狀態。這確保了 store 狀態的隔離和內存管理。
在 Pinia 的內部,Map
和 WeakMap
被用於管理 store 的依賴和作用域。這些數據結構的選擇使得 Pinia 能夠高效地處理狀態管理和依賴追蹤。
Map
:用於存儲 key/value,key/value 都可以是任意類型。Pinia 使用 Map
來管理 store 實例和狀態。WeakMap
:鍵必須是 Object,且其引用是弱引用。Pinia 使用 WeakMap
來處理與 store 相關的依賴,避免內存泄漏。Set
和 WeakSet
:用於存儲唯一值,WeakSet
中的對象引用也是弱引用。Pinia 使用這些結構來管理動作和狀態追蹤。Map
和 WeakMap
的基本使用// 使用 Map 管理 store 的實例
interface CustomObject {
count: number;
}
const storeMap = new Map<string, CustomObject>();
const myStore = { count: 0 };
storeMap.set('myStore', myStore);
console.log(storeMap.get('myStore')); //結果是 { count: 0 }
// 使用 WeakMap 管理 store 的依賴
interface MappingKey {
id: string;
state: string;
}
const storeWeakMap = new WeakMap<MappingKey, CustomObject>();
const storeInstanceKey: MappingKey = {
id: 'this is id',
state: 'start...'
};
storeWeakMap.set(storeInstanceKey, { count: 1 });
console.log(storeWeakMap.get(storeInstance)); // { count: 1 }
private store
和 readonly store
的概念及實作關於我們在 Day8 提供單向數據流
,揭開了 pinia 可維護性的正確寫法,但那只是冰山一角。接下來我們正是系統性地處理並解決可維護性的 pinia store 該如何展現。
Pinia 支持 private store
和 readonly store
的設計,使得我們可以創建只能在內部修改的狀態,並對外部提供只讀訪問。確保整體的設計是一致有系統性且嚴謹的
private store
的概念是指某些狀態只能在 store 內部修改,外部只能通過 actions 來操作。
(檔案src/stores/useCounterStore.ts
)
import { computed, shallowRef } from 'vue'
import { defineStore, acceptHMRUpdate } from 'pinia';
// private pinia
const usePrivateCounterStore = defineStore("usePrivateCounterStore", () => {
// private state::
const count = shallowRef<number>(0);
return {
count,
}
});
export const useCounterStore = defineStore('useCounterStore', () => {
const privateCounterStore = usePrivateCounterStore();
// state::
// getter::
const doubleCount = computed<number>(() => privateCounterStore.count * 2);
// methods::
const increment = (): void => {
privateCounterStore.count++;
};
return {
// state::
count: computed<number>(() => privateCounterStore.count),
// getters::
doubleCount,
// methods::
increment
}
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(usePrivateCounterStore, import.meta.hot));
import.meta.hot.accept(acceptHMRUpdate(useCounterStore, import.meta.hot));
}
在這個例子中,privateCount
是私有的,外部無法直接修改它,只能通過內部的 increment
方法來改變其值。可以更近一步確保封裝的安全,且不會受到外部 composables
, stores
, components
直接更改。
readonly store
允許我們定義一個僅提供讀取的 store 狀態,這對於需要保證狀態不可變的情況非常有用。
(檔案 src/stores/readonlyStore.ts
)
import { defineStore } from 'pinia';
import { readonly, shallowRef } from 'vue';
export const useReadonlyStore = defineStore('readonly', () => {
const count = shallowRef(0);
const increment = () => {
count.value++;
};
return { count: readonly(count), increment };
});
這裡的 count
是只讀的,雖然可以通過 increment
修改,但直接操作 count
的值將被禁止,這保護了狀態的完整性。
在使用 privateStore 的狀態下每次都要去產生兩個 store
一個去處理 private state
一個去定義 public method
或是 getters
的部分,這樣每次撰寫時都要重複做一件事,那我們把整個方法封裝起來,建立 privateState
的方法
(檔案 src/stores/privateState.ts
)
import { UnwrapRef } from 'vue';
import { Router } from 'vue-router'
import { defineStore, StateTree, PiniaCustomStateProperties } from 'pinia';
export function definePrivateState<
Id extends string,
PrivateState extends StateTree,
SetupReturn
>(
id: Id,
privateStateFn: () => PrivateState,
setup: (privateState: UnwrapRef<PrivateState> & PiniaCustomStateProperties<PrivateState>, router: Router) => SetupReturn
) {
const usePrivateState = defineStore(`${id}_private`, {
state: privateStateFn,
});
return defineStore(id, () => {
const privateState = usePrivateState();
return setup(privateState.$state);
});
}
這樣就可以這樣使用 private state pinia store
(檔案 src/stores/useNewCounterStore.ts
)
import { computed } from "vue"
import { acceptHMRUpdate } from "pinia";
import { definePrivateState } from "./privateState";
import { RoutesStatus } from "../router";
export const useNewCounterStore = definePrivateState("useNewCounterStore", () => {
return {
count: 0,
}
}, privateState => {
const doubleCount = computed<number>(() => privateState.count * 2);
const increment = (): void => {
privateState.count++;
};
return {
count: computed(() => privateState.count),
doubleCount,
increment,
}
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useNewCounterStore, import.meta.hot));
}
Pinia
至高領域 - 自定義 pinia 的行為基本上有以上的操作會認為對 pinia 的理解已經非常通透了。
(圖片取自鬼滅之刃)
但要到達 pinia 至高領域
,還需要可以自定義 pinia 的行為,但這些自定義行為需要道行高一點才能駕馭,就像炭治郎一開始要掌握日之呼吸不大可能的,建議以下深入的操作,在對 vue 和 pinia 有更深的理解再進行以下操作。(總共有 10 個型),由於篇幅有限,我只展示第一型最簡單的部分
這裡就魔改一下 privateState
(檔案 src/stores/privateState.ts
)
import { UnwrapRef } from 'vue';
import { Router } from 'vue-router'
import { defineStore, StateTree, PiniaCustomStateProperties } from 'pinia';
export function definePrivateState<
Id extends string,
PrivateState extends StateTree,
SetupReturn
>(
id: Id,
privateStateFn: () => PrivateState,
setup: (privateState: UnwrapRef<PrivateState> & PiniaCustomStateProperties<PrivateState>, router: Router) => SetupReturn
) {
const usePrivateState = defineStore(`${id}_private`, {
state: privateStateFn,
actions: {
useRouter() {
// 這裡注入 vue-router,讓往後使用的 store 可以直接呼叫 vue-router 的方法
return this.router;
}
}
});
return defineStore(id, () => {
const privateState = usePrivateState();
return setup(privateState.$state, privateState.useRouter());
});
}
為了做到上述的事情,要增加 vue-router 的 custom plugin 在 createPinia 的時候
(檔案 src/pinia/index.ts
)
import { markRaw } from 'vue'
import { Router } from 'vue-router'
import { createPinia } from 'pinia';
import router from '../router';
export const pinia = createPinia();
pinia.use(({ store }) => {
store.router = markRaw(router);
});
declare module 'pinia' {
interface PiniaCustomProperties {
router: Router,
}
}
這時候在 main.ts
可以稍微修改
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { pinia } from './pinia' // 這裡改成這樣不用直接 createPinia
createApp(App)
.use(router)
.use(pinia) // 這裡修改
.mount('#app')
上述完成後,就可以可以做一個 privateState 使用 router 做一些操作了,這裡我們複製原來的 useNewCounterStore,複製貼上製作一個新的 useNewBaseStore
作為展示
(檔案 src/stores/useNewBaseStore.ts
)
import { computed } from "vue"
import { acceptHMRUpdate } from "pinia";
import { definePrivateState } from "./privateState";
import { RoutesStatus } from "../router";
export const useNewBaseStore = definePrivateState("useNewBaseStore", () => {
return {
count: 0,
}
}, (privateState, router) => {
const doubleCount = computed<number>(() => privateState.count * 2);
const increment = (): void => {
privateState.count++;
};
// 這裡即可直接呼叫 vue-router 進行陸游的操作,不用額外的 `const router = useRouter`
const goToFoo = (): void => {
router.push({ name: RoutesStatus.Foo });
};
return {
count: computed(() => privateState.count),
doubleCount,
increment,
goToFoo,
}
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useNewBaseStore, import.meta.hot));
}
通過結合 TypeScript 與 Pinia,並深入了解 effectScope
、依賴注入、Map
、WeakMap
、Set
、WeakSet
以及 private store
和 readonly store
的概念,我們能夠更加靈活和高效地管理應用中的狀態。這些概念不僅提升了 Pinia 的可用性,也保證了應用的健壯性和可維護性。
希望這篇文章能幫助你更好地理解 Pinia 的底層原理和實際應用,讓你在開發過程中能夠更加得心應手!